class Server {
  #peers = [];

  createPeer() {
    const peer = new Peer(this, `peer${this.#peers.length + 1}`);
    this.#peers.push(peer);
    return peer;
  }

  forwardMessage(fromPeer, data) {
    this.#peers.forEach((peer) => {
      if (peer !== fromPeer) {
        peer.receiveMessage(data);
      }
    });
  }
}

class Peer {
  #server = "";
  #name = "";
  #video = null;
  #pc = null;

  constructor(server, name) {
    this.#server = server;
    this.#name = name;

    const video = document.createElement("video");
    video.autoplay = true;
    video.controls = false;
    document.body.append(video);
    this.#video = video;

    const pc = new RTCPeerConnection();
    pc.addEventListener(
      "negotiationneeded",
      this.#onNegotiationNeeded.bind(this)
    );
    pc.addEventListener(
      "signalingstatechange",
      this.#onSignalingStateChange.bind(this)
    );
    pc.addEventListener(
      "icegatheringstatechange",
      this.#onIceGatheringStateChange.bind(this)
    );
    pc.addEventListener(
      "iceconnectionstatechange",
      this.#onIceConnectionStateChange.bind(this)
    );
    pc.addEventListener(
      "connectionstatechange",
      this.#onConnectionStateChange.bind(this)
    );
    pc.addEventListener("icecandidate", (e) => this.#onIceCandidate(e));
    pc.addEventListener("track", (e) => this.#onRemoteTrack(e));
    this.#pc = pc;

    this.log("created");
    this.#onSignalingStateChange();
    this.#onIceGatheringStateChange();
    this.#onIceConnectionStateChange();
    this.#onConnectionStateChange();
  }

  destroy() {
    this.#pc.close();
    this.#pc = null;

    const stream = this.#video.srcObject;
    stream?.getTracks().forEach((track) => track.stop());

    this.#video.remove();
    this.#video = null;
  }

  log(...messages) {
    console.log(`[${this.#name}]`, ...messages);
  }

  #onSignalingStateChange() {
    this.log("signaling state change", this.#pc.signalingState);
  }
  #onIceGatheringStateChange() {
    this.log("ice gathering state change", this.#pc.iceGatheringState);
  }
  #onIceConnectionStateChange() {
    this.log("ice connection state change", this.#pc.iceConnectionState);
  }
  #onConnectionStateChange() {
    this.log("connection state change", this.#pc.iceConnectionState);
  }

  async #onNegotiationNeeded(e) {
    const offer = await this.#pc.createOffer();
    this.#pc.setLocalDescription(offer);
    this.log("offer", offer);
    this.publishMessage({ type: "offer", payload: offer });
  }

  #onIceCandidate(e) {
    this.log("ice candidate", e.candidate);
    this.publishMessage({ type: "candidate", payload: e.candidate });
  }

  #onRemoteTrack(e) {
    this.log("remote track");
    const [stream] = e.streams;
    if (this.#video.srcObject !== stream) {
      this.#video.srcObject = stream;
    }
  }

  #onRemoteSignalIceCandidate(data) {
    this.log("remote signal ice candidate", data);
    if (data) {
      const candidate = new RTCIceCandidate(data);
      this.#pc.addIceCandidate(candidate);
    }
  }

  async #onRemoteSignalOffer(data) {
    this.log("remote signal offer", data);
    const offer = new RTCSessionDescription(data);
    this.#pc.setRemoteDescription(offer);
    const answer = await this.#pc.createAnswer();
    this.#pc.setLocalDescription(answer);
    this.publishMessage({ type: "answer", payload: answer });
  }

  #onRemoteSignalAnswer(data) {
    this.log("remote signal answer", data);
    const answer = new RTCSessionDescription(data);
    this.#pc.setRemoteDescription(answer);
  }

  async call() {
    this.log("start call");
    const stream = await navigator.mediaDevices.getUserMedia({
      audio: true,
      video: true,
    });
    this.#video.srcObject = stream;

    stream.getTracks().forEach((track) => {
      this.#pc.addTrack(track, stream);
    });
  }

  publishMessage(data) {
    this.#server.forwardMessage(this, data);
  }

  receiveMessage(data) {
    const { type, payload } = data;
    switch (type) {
      case "candidate":
        this.#onRemoteSignalIceCandidate(payload);
        break;
      case "offer":
        this.#onRemoteSignalOffer(payload);
        break;
      case "answer":
        this.#onRemoteSignalAnswer(payload);
        break;
    }
  }
}

const server = new Server();
const peer1 = server.createPeer();
const peer2 = server.createPeer();
peer1.call();
